package cn.kinoko.aspect;

import cn.kinoko.annotation.Cacheable;
import cn.kinoko.component.BloomFilterLoader;
import cn.kinoko.config.ConfigProperties;
import cn.kinoko.error.BloomHitFailException;
import cn.kinoko.service.RedisService;
import cn.kinoko.utils.SpelUtil;
import lombok.Getter;
import lombok.Setter;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.InvocationTargetException;
import java.time.Duration;
import java.util.Optional;

/**
 * 分布式缓存切面
 * @author kk
 */
@Getter
@Setter
@Aspect
@Component
public class CacheableAspect {

    @Autowired
    private ConfigProperties configProperties;
    @Autowired
    private RedisService redisService;

    @Around("@annotation(cacheable)")
    public Object around(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
        // 获取redisKey
        String redisKey = SpelUtil.getRedisKey(joinPoint, cacheable.key(), cacheable.params());
        // 布隆过滤
        bloomFilter(cacheable, redisKey);
        // 获取缓存
        Optional<?> obj = redisService.matchTypeAndGet(redisKey);
        // 缓存不为空范围
        if (obj.isPresent()) {
            return obj.get();
        }
        // 执行业务方法
        Object target = joinPoint.proceed();
        // 检查条件，加入缓存
        if (target != null && !SpelUtil.getBoolean(joinPoint, cacheable.unless(), false)) {
            boolean enableTypeMatch = cacheable.typeMatch() || configProperties.isTypeMatch();
            // 设置缓存
            if (cacheable.expire() == -1) {
                putCache(redisKey, target, enableTypeMatch);
            } else {
                putCache(redisKey, target, SpelUtil.toDuration(cacheable.expire(), cacheable.timeUnit()), enableTypeMatch);
            }
            // 加入布隆过滤器
            if (bloomEnable(cacheable) && !BloomFilterLoader.BLOOM_FILTER.contains(redisKey)) {
                BloomFilterLoader.BLOOM_FILTER.add(redisKey);
            }
        }
        return target;
    }

    /**
     * 布隆过滤
     * 策略：
     * 判断是否为第一次访问，是则放行尝试构建缓存，计数+1，成功则加入布隆过滤器
     * 若不是第一次访问则直接抛出异常，拒绝访问
     *
     * @param cacheable cacheable
     * @param redisKey  redisKey
     */
    private void bloomFilter(Cacheable cacheable, String redisKey) {
        if (bloomEnable(cacheable) && !BloomFilterLoader.BLOOM_FILTER.contains(redisKey)) {
            if (BloomFilterLoader.REF_COUNT.getIfPresent(redisKey) == null) {
                BloomFilterLoader.REF_COUNT.put(redisKey, 1);
            } else {
                throw new BloomHitFailException(cacheable.bloomHitFail());
            }
        }
    }

    /**
     * 是否开启布隆过滤器
     * @param cacheable cacheable
     * @return 是否开启布隆过滤器
     */
    private boolean bloomEnable(Cacheable cacheable) {
        return (cacheable.enableBloom() || configProperties.getBloomFilter().isEnable()) && BloomFilterLoader.BLOOM_FILTER != null;
    }

    /**
     * 设置缓存
     * @param redisKey  redisKey
     * @param target    target
     * @param typeMatch 类型匹配
     */
    private void putCache(String redisKey, Object target, boolean typeMatch) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        putCache(redisKey, target, null, typeMatch);
    }

    /**
     * 设置缓存
     * @param redisKey redisKey
     * @param target   target
     * @param expire   expire
     * @param typeMatch 类型匹配
     */
    private void putCache(String redisKey, Object target, Duration expire, boolean typeMatch) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
        if (typeMatch) {
            redisService.matchTypeAndSet(redisKey, target, expire);
        } else {
            redisService.set(redisKey, target);
        }
    }

}
